﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using weka.core;
using Attribute = weka.core.Attribute;
using weka.classifiers;
using weka.filters;
using weka.classifiers.bayes;
using weka.filters.unsupervised.attribute;
using weka.core.converters;
using System.Diagnostics.Contracts;


namespace OMC.Classification
{
    /// <summary>
    /// Class <c>TextClassifierBuilder</c> creates <see cref="TextClassifier"/>s.
    /// </summary>
    /// <example>
    /// The example shows the basic usage of class <c>TextClassifierBuilder</c>:
    /// <code>
    /// TextClassifierBuilder builder = new TextClassifierBuilder();
    /// // 1. Define classes
    /// builder.SetClasses(new String[] { "class1", "class2" });
    /// // 2. Add training instances
    /// builder.AddTrainingInstance("This text belongs to class1", "class1");
    /// builder.AddTrainingInstance("This text belongs to class2", "class2");
    /// builder.ProcessText();
    /// // 3. Build the classifier
    /// TextClassifier classifier = builder.BuildClassifier();
    /// </code>
    /// The <c>classifier</c> can now be used to classify texts.
    /// </example>
    /// The example above will internally be processes in the following steps:
    /// 1. Create unfiltered instance
    /// <code>builder.AddText("This text belongs to class1", "class1");</code>
    /// will create an Instance as with two attributes:
    ///     Attribute Name  Attribute Value
    ///     "text"          "This text belongs to class1"
    ///     "class"         "class1"
    /// 2. Filtering processes the text attribute and will create an Instance as follows:
    ///     Attribute Name  Attribute Value
    ///     "class"         "class1"
    ///     "this"          1               // occurence
    ///     "text"          1
    ///     "belongs"       1
    ///     "to"            1   
    ///     "class1"        1
    ///     "other1"        0               // 
    ///     "other2"        0
    ///
    [Serializable()]
    public class TextClassifierBuilder
    {
        private Attribute textAttribute;
        private Attribute classAttribute;
        private Instances unfilteredInstances;
        private StringToWordVector stringToWordFilter;
        private Instances filteredInstances;
        private Classifier classifierTemplate;

        /// <summary>
        /// Create a text classifier builder.
        /// </summary>
        public TextClassifierBuilder()
        {
            textAttribute = new Attribute("text", (FastVector)null);
            stringToWordFilter = new StringToWordVector();
            classifierTemplate = new NaiveBayesMultinomialUpdateable();
        }

        /// <summary>
        /// Set the classifier
        /// </summary>
        /// <param name="classifier">the classifier; not <c>null</c>; implements UpdateableClassifier</param>
        public void SetClassifier(Classifier classifier)
        {
            Contract.Requires(classifier != null);
            Contract.Requires(classifier is UpdateableClassifier);

            this.classifierTemplate = classifier;
        }

        /// <summary>
        /// Set stopwords.
        /// </summary>
        /// <param name="stopwordsEnumeratur"></param>
        public void SetStopwords(IEnumerable<string> stopwordsEnumeratur)
        {
            java.io.File stopwordsFile = java.io.File.createTempFile("stopwords", "txt");
            stopwordsFile.deleteOnExit();
            java.io.FileWriter stopwordsWriter = null;
            try
            {
                stopwordsWriter = new java.io.FileWriter(stopwordsFile);
                foreach (string stopword in stopwordsEnumeratur)
                {
                    stopwordsWriter.append(stopword);
                    stopwordsWriter.append('\n');
                }
            }
            catch (java.io.IOException)
            {
                if (stopwordsWriter != null)
                {
                    stopwordsWriter.close();
                }
            }
            stringToWordFilter.setStopwords(stopwordsFile);
        }

        /// <summary>
        /// Set the classes.
        /// </summary>
        /// <param name="classEnumerator"></param>
        public void SetClasses(IEnumerable<string> classEnumerator)
        {
            Contract.Requires(classEnumerator != null);
            Contract.Requires(classEnumerator != null);

            FastVector classes = new FastVector();
            foreach (string @class in classEnumerator)
            {
                classes.addElement(@class);
            }
            classAttribute = new Attribute("class", classes);

            FastVector attributes = new FastVector();
            attributes.addElement(textAttribute);
            attributes.addElement(classAttribute);

            unfilteredInstances = new Instances("unfiltered", attributes, 100);
            unfilteredInstances.setClass(classAttribute);

            Contract.Ensures(unfilteredInstances.attribute("text") != null);
            // Check class attribute
            Contract.Requires(unfilteredInstances.classAttribute() == classAttribute, "Class attribute is set.");
            Contract.Requires(classAttribute.isNominal() && classAttribute.numValues() > 0, "Class attribute is a nominal attribute");
        }

        /// <summary>
        /// Was a folder with the given name registered as class attribute value?
        /// </summary>
        /// <param name="folder"></param>
        public bool IsFolderExisting(string folder)
        {
            if (unfilteredInstances.attribute("class").indexOfValue(folder) == -1)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        /// Add a training instance to unfiltered instances (only used during initial training phase, not for updates when moving mails around)
        /// </summary>
        /// <param name="text">The text of the training instance; not <c>null</c>.</param>
        /// <param name="class">The class of the training instance; not <c>null</c>.</param>
        /// <param name="weight">Set to 1 for adding; Set to -1 for removing</param>
        public void AddTrainingInstance(String text, string @class, double weight)
        {
            Instance instance = GenerateUnfilteredInstance(text, @class, weight);
            unfilteredInstances.add(instance);
        }

        /// <summary>
        /// Generate an unfiltered instance (with only 2 classes) from a (mail-)text
        /// </summary>
        /// <param name="text">The text of the training instance; not <c>null</c>.</param>
        /// <param name="class">The class of the training instance; not <c>null</c>.</param>
        /// <param name="weight">Set to 1 for adding; Set to -1 for removing</param>
        /// <returns></returns>
        private Instance GenerateUnfilteredInstance(String text, string @class, double weight)
        {
            Contract.Requires(text != null);
            Contract.Requires(@class != null);
            Contract.Requires(classAttribute.indexOfValue(@class) != -1);

            // Filtering a text containing token "class" might create a new attribute with name class and thus overrides the value for the classification attribute.
            text = text.Replace("class", "");

            Instance instance = new Instance(2);
            instance.setValue(textAttribute, text);
            instance.setDataset(unfilteredInstances);
            instance.setValue(classAttribute, @class);
            instance.setWeight(weight);

            return instance;
        }

        /// <summary>
        /// Filter unfilteredInstances (is only called during the initial training).
        /// </summary>
        public void FilterInstances()
        {
            stringToWordFilter.setInputFormat(unfilteredInstances);
            stringToWordFilter.setLowerCaseTokens(true);
            filteredInstances = Filter.useFilter(unfilteredInstances, stringToWordFilter);
            // unfilteredInstances.delete(); would save space, but method SaveUnfiltered needs the instances
        }


        public Instance Text2FilteredInstance(String text, string @class, double weight)
        {
            Instance myUnfilteredInstance = GenerateUnfilteredInstance(text, @class, weight);
            stringToWordFilter.input(myUnfilteredInstance);
            stringToWordFilter.batchFinished();
            return stringToWordFilter.output();
        }


        /// <summary>
        /// Add a training instance to unfiltered instances (only used when moving mails around)
        /// </summary>
        /// <param name="text">The text of the training instance; not <c>null</c>.</param>
        /// <param name="class">The class of the training instance; not <c>null</c>.</param>
        /// <param name="weight">Set to 1 for adding; Set to -1 for removing</param>
        //public void AddTrainingInstanceBelatedly(String text, string @class, double weight)
        //{
            //Instance myUnfilteredInstance = GenerateUnfilteredInstance(text, @class, weight);

            // --- Add to TextClassifier:classifier via updateClassifier() (benötigt Instance)


            // --- Add to TextClassifierBuilder:filteredInstances
            
            //Instances unfilteredInstancesEmpty = new Instances(unfilteredInstances, 0);
            //Instances filteredInstancesEmpty = new Instances(filteredInstances, 0);
            
            //StringToWordVector myStringToWordFilter = new StringToWordVector();
            //myStringToWordFilter.setInputFormat(unfilteredInstances);
            //myStringToWordFilter.setLowerCaseTokens(true);

            //myStringToWordFilter.input(myUnfilteredInstance);
            //Instance myFilteredInstance = stringToWordFilter.output();

            //unfilteredInstances.add(myUnfilteredInstance);
        //}


        /// <summary>
        /// Build the classifier.
        /// </summary>
        /// <returns>A trained classifer.</returns>
        public TextClassifier BuildClassifier()
        {
            // build classifer
            Classifier classifier = Classifier.makeCopy(classifierTemplate);
            classifier.buildClassifier(filteredInstances);
            filteredInstances.stringFreeStructure();
            // filteredInstances.delete(); would save space, but method SaveFiltered needs the instances

            // the classifier only needs to know the structure of the instances, so create empty instances
            Instances unfilteredInstancesEmpty = new Instances(unfilteredInstances, 0);
            Instances filteredInstancesEmpty = new Instances(filteredInstances, 0);
            return new TextClassifier(classifier, stringToWordFilter, unfilteredInstancesEmpty, filteredInstancesEmpty);
        }

        /// <summary>
        /// Evaluate this classifier.
        /// </summary>
        /// <returns></returns>
        public Evaluation Evaluate()
        {
            // randomize
            Filter randomFilter = new weka.filters.unsupervised.instance.Randomize();
            randomFilter.setInputFormat(filteredInstances);
            Instances randomizedFilteredInstances = Filter.useFilter(filteredInstances, randomFilter);

            // split data set
            const double splitFactor = 0.67;  // 67% for training
            int split = (int)(randomizedFilteredInstances.numInstances() * splitFactor);
            Instances trainingSet = new Instances(randomizedFilteredInstances, 0, split);
            Instances testSet = new Instances(randomizedFilteredInstances, split, randomizedFilteredInstances.numInstances() - split);

            // train classifier
            Classifier classifier = Classifier.makeCopy(classifierTemplate);
            classifier.buildClassifier(trainingSet);

            // evaluate classifier
            Evaluation evaluation = new Evaluation(trainingSet);
            evaluation.evaluateModel(classifier, testSet);
            return evaluation;
        }

        /// <summary>
        /// Save instances.
        /// </summary>
        /// <param name="instances">The Instances to save; not <c>null</c>.</param>
        /// <param name="path">The path to save to; not <c>null</c>.</param>
        private void SaveInstances(Instances instances, String path)
        {
            Contract.Requires(instances != null);
            Contract.Requires(path != null);

            ArffSaver saver = new ArffSaver();
            saver.setInstances(instances);
            saver.setFile(new java.io.File(path));
            saver.writeBatch();
        }

        /// <summary>
        /// Save unfiltered instances.
        /// </summary>
        /// <param name="path">The path to save to; not <c>null</c>.</param>
        public void SaveUnfiltered(String path)
        {
            SaveInstances(unfilteredInstances, path);
        }

        /// <summary>
        /// Save filtered instances.
        /// </summary>
        /// <param name="path">The path to save to; not <c>null</c>.</param>
        public void SaveFiltered(String path)
        {
            SaveInstances(filteredInstances, path);
        }

    }
}
